package crmdna.mail;

import static crmdna.common.AssertUtils.ensure;
import static crmdna.common.AssertUtils.ensureEqual;
import static crmdna.common.AssertUtils.ensureNotNull;
import static crmdna.common.OfyService.ofy;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.microtripit.mandrillapp.lutung.MandrillApi;
import com.microtripit.mandrillapp.lutung.model.MandrillApiError;
import com.microtripit.mandrillapp.lutung.view.MandrillMessage;
import com.microtripit.mandrillapp.lutung.view.MandrillMessage.MergeVar;
import com.microtripit.mandrillapp.lutung.view.MandrillMessage.MergeVarBucket;
import com.microtripit.mandrillapp.lutung.view.MandrillMessage.Recipient;
import com.microtripit.mandrillapp.lutung.view.MandrillMessageStatus;
import com.microtripit.mandrillapp.lutung.view.MandrillTag;

import crmdna.common.Utils;
import crmdna.common.api.APIException;
import crmdna.common.api.APIResponse.Status;
import crmdna.mail.Mail.MailStatus;

public class Mandrill {


  public static final int MAX_EMAILS_PER_SEND = 5000;

  public static Map<String, MandrillMessageStatus> send(Set<String> emails,
      Map<String, Map<String, String>> emailVsMergeVar, String subject, String messageBody,
      String from, String subaccount) throws MandrillApiError, IOException {

    ensureNotNull(messageBody, "messageBody is null");
    ensureNotNull(from, "from is null");
    ensureNotNull(emails, "emails is null");
    ensureNotNull(subject, "subject is null");

    // remove invalid email addresses
    Set<String> invalid = new HashSet<>();
    for (String email : emails) {
      if (!Utils.isValidEmailAddress(email))
        invalid.add(email);
    }

    for (String email : invalid) {
      emails.remove(email);
    }

    Logger logger = Logger.getLogger(Mandrill.class.getName());
    logger.info("emails.size() = " + emails.size());

    if (emails.isEmpty())
      return new HashMap<>();

    if (emails.size() > MAX_EMAILS_PER_SEND) {
      throw new APIException().status(Status.ERROR_OVERFLOW).message(
          "Attempt to send [" + emails.size() + "] emails in one shot. Maximum allowed is ["
              + MAX_EMAILS_PER_SEND + "]");
    }

    MandrillMessage message = new MandrillMessage();

    message.setSubject(subject);
    message.setHtml(messageBody);
    message.setAutoText(true);
    message.setTrackClicks(true);
    message.setTrackOpens(true);
    message.setFromEmail(from);

    Set<String> mergeTagsInHtmlBody = getMergeTags(messageBody);

    List<MergeVarBucket> mergeVarBuckets = new ArrayList<>(emailVsMergeVar.size());

    for (String email : emails) {
      MergeVarBucket mergeVarBucket = new MergeVarBucket();
      mergeVarBucket.setRcpt(email);

      List<MergeVar> mergeVars = new ArrayList<>();

      for (String mergeTag : mergeTagsInHtmlBody) {
        ensureNotNull(emailVsMergeVar, "emailVsMergeVar is null but [" + mergeTagsInHtmlBody.size()
            + "] merge tags are specified in message body");

        ensure(emailVsMergeVar.containsKey(email), "No merge var(s) specified for [" + email
            + "] but [" + mergeTagsInHtmlBody.size() + "] merge tags are specified in message body");

        ensure(emailVsMergeVar.get(email).containsKey(mergeTag), "Merge tag [" + mergeTag
            + "] missing for recepient [" + email + "]");

        mergeVars.add(new MergeVar(mergeTag, emailVsMergeVar.get(email).get(mergeTag)));
      }

      mergeVarBucket.setVars(mergeVars.toArray(new MergeVar[mergeVars.size()]));
      mergeVarBuckets.add(mergeVarBucket);
    }

    message.setMergeVars(mergeVarBuckets);

    String apiKey = "SGDxaNgCVEF6trBYKtugag";
    ensureValidApiKey(apiKey);

    MandrillApi mandrillApi = new MandrillApi(apiKey);

    if ((subaccount != null) && !subaccount.isEmpty()) {
      if (!isSubaccountValid(apiKey, subaccount)) {
        createSubaccount(apiKey, subaccount);
      }

      ensureValidSubaccount(apiKey, subaccount); // just in case
      message.setSubaccount(subaccount);
    }



    // // dummy attachment
    // MessageContent attachment = new MessageContent();
    // String content =
    // "first name, last name\nsathya, thilakan\nparamesh, dhevan\nthulasidhar, kosalram";
    // attachment.setContent(content);
    // attachment.setName("DummyAttachment.csv");
    // attachment.setType("text/plain");
    //
    // List<MessageContent> attachments = new ArrayList<>();
    // attachments.add(attachment);
    // message.setAttachments(attachments);

    List<Recipient> recipients = new ArrayList<>();

    for (String email : emails) {
      Recipient recipient = new Recipient();
      recipient.setEmail(email);
      recipients.add(recipient);
    }

    message.setTo(recipients);

    boolean async = true;
    MandrillMessageStatus[] status = mandrillApi.messages().send(message, async);

    ensureEqual(emails.size(), status.length,
        "Num elements in MandrillMessageStatus does not match number of emails");

    Map<String, MandrillMessageStatus> map = new HashMap<>();
    for (MandrillMessageStatus mandrillMessageStatus : status) {
      map.put(mandrillMessageStatus.getEmail(), mandrillMessageStatus);
    }

    return map;
  }

  private static Set<String> getMergeTags(String html) {
    ensureNotNull(html, "html is null");

    final String MERGE_TAG_PATTERN = "\\*\\|(.+?)\\|\\*";
    Matcher matcher = Pattern.compile(MERGE_TAG_PATTERN).matcher(html);

    Set<String> mergeTags = new HashSet<>();
    while (matcher.find()) {
      String s = matcher.group(1);
      mergeTags.add(s);
    }

    return mergeTags;
  }

  public static MandrillTag getStats_not_used(String tag) throws MandrillApiError, IOException {
    ensureNotNull(tag, "tag is null");

    MandrillApi mandrillApi = new MandrillApi("SGDxaNgCVEF6trBYKtugag");

    MandrillTag[] allTags = mandrillApi.tags().list();

    boolean valid = false;
    int i;
    for (i = 0; i < allTags.length; i++) {
      if (allTags[i].getTag().equalsIgnoreCase(tag)) {
        valid = true;
      }
    }

    if (!valid)
      throw new APIException().status(Status.ERROR_RESOURCE_NOT_FOUND).message(
          "Invalid tag [" + tag + "]");

    return allTags[i];
  }

  public static String createSubaccount(String apiKey, String subaccount) throws IOException,
      MandrillApiError {
    ensureValidApiKey(apiKey);

    ensureNotNull(subaccount, "subaccount is null");
    // TODO: remove all non alpha numeric (here as well as in crmdna)
    subaccount = Utils.removeSpaceUnderscoreBracketAndHyphen(subaccount).toLowerCase();

    ensure(subaccount.length() > 0, "subaccount is empty string");

    subaccount = Utils.removeSpaceUnderscoreBracketAndHyphen(subaccount).toLowerCase();

    ensureValidApiKey(apiKey);

    if (isSubaccountValid(apiKey, subaccount)) {
      throw new APIException().status(Status.ERROR_RESOURCE_ALREADY_EXISTS).message(
          "subaccount [" + subaccount + "] already exists");
    }

    MandrillApi mandrillApi = new MandrillApi(apiKey);
    mandrillApi.subaccounts().add(subaccount, null, null, null);

    return subaccount;
  }

  public static void processWebhookEvents(MandrillEventProp[] eventProps) {

    Map<String, MandrillEventProp> mailIdVsEventProp = new HashMap<>();

    // group by client
    Map<String, Set<String>> clientVsMailIds = new HashMap<>();

    for (int i = 0; i < eventProps.length; i++) {
      MandrillEventProp mandrillEventProp = eventProps[i];

      String client = mandrillEventProp.getClient();

      if (client == null) // TODO: or client is invalid
        continue;

      if (!clientVsMailIds.containsKey(client))
        clientVsMailIds.put(client, new HashSet<String>());

      Set<String> mailIds = clientVsMailIds.get(client);
      mailIds.add(mandrillEventProp._id);

      mailIdVsEventProp.put(mandrillEventProp._id, mandrillEventProp);
    }

    ensureEqual(eventProps.length, mailIdVsEventProp.size(), "Error when populating map");

    // get relevant sent mail entities
    Map<String, Map<String, SentMailEntity>> clientVsSentMailEntities = new HashMap<>();
    for (String client : clientVsMailIds.keySet()) {
      Set<String> mailIds = clientVsMailIds.get(client);

      Map<String, SentMailEntity> mailIdVsSentMailEntity =
          ofy(client).load().type(SentMailEntity.class).ids(mailIds);
      clientVsSentMailEntities.put(client, mailIdVsSentMailEntity);
    }

    for (String client : clientVsSentMailEntities.keySet()) {
      List<SentMailEntity> toSave = new ArrayList<>();

      Map<Long, MailStatsProp> bulkEmailIdVsMailStatusProp = new HashMap<>();

      Map<String, SentMailEntity> mailIdVsSentMailEntity = clientVsSentMailEntities.get(client);

      for (String mailId : mailIdVsSentMailEntity.keySet()) {
        MandrillEventProp eventProp = mailIdVsEventProp.get(mailId);
        SentMailEntity sentMailEntity = mailIdVsSentMailEntity.get(mailId);
        updateSentMailEntity(sentMailEntity, eventProp);

        Long bulkEmailId = sentMailEntity.bulkEmailId;
        if (bulkEmailId != null) {
          if (!bulkEmailIdVsMailStatusProp.containsKey(bulkEmailId))
            bulkEmailIdVsMailStatusProp.put(bulkEmailId, new MailStatsProp());

          MailStatsProp mailStatsProp = bulkEmailIdVsMailStatusProp.get(bulkEmailId);
          updateMailStatsProp(mailStatsProp, eventProp, sentMailEntity);
        }

        toSave.add(sentMailEntity);
      }

      ofy(client).save().entity(toSave);

      // TODO: for each bulk email id save mail stats
    }
  }

  private static void updateMailStatsProp(MailStatsProp mailStatsProp, MandrillEventProp eventProp,
      SentMailEntity sentMailEntity) {
    ensureNotNull(mailStatsProp, "mailStatsProp is null");
    ensureNotNull(eventProp, "mandrillEventProp is null");
    ensureNotNull(sentMailEntity, "sentMailEntity is null");

    if (eventProp.event == null)
      return;

    String event = eventProp.event;
    if (event.equals("send")) {
      mailStatsProp.numSent++;
      return;
    }

    if (event.equals("deferral")) {
      // no action required
      return;
    }

    if (event.equals("hard_bounce")) {
      mailStatsProp.numHardBounces++;
      return;
    }

    if (event.equals("soft_bounce")) {
      mailStatsProp.numSoftBounces++;
      return;
    }

    if (event.equals("open")) {
      mailStatsProp.numOpens++;

      if (eventProp.user_agent_parsed.mobile)
        mailStatsProp.numMobileOpens++;

      final int MILLISECONDS_IN_AN_HOUR = 3600 * 1000;
      long sentTS = sentMailEntity.queuedTS;
      if (eventProp.ts - sentTS < MILLISECONDS_IN_AN_HOUR)
        mailStatsProp.numOpensWithin1Hour++;
      else if (eventProp.ts - sentTS < 3 * MILLISECONDS_IN_AN_HOUR)
        mailStatsProp.numOpensWithin3Hours++;
      else if (eventProp.ts - sentTS < 24 * MILLISECONDS_IN_AN_HOUR)
        mailStatsProp.numOpensWithin24Hours++;
      else if (eventProp.ts - sentTS < 72 * MILLISECONDS_IN_AN_HOUR)
        mailStatsProp.numOpensWithin72Hours++;
      else
        mailStatsProp.numOpensAfter72Hours++;

      // country
      Map<String, Integer> map = mailStatsProp.countryVsNumOpens;
      String country = eventProp.location.country_short;
      if (!map.containsKey(country))
        map.put(country, 0);

      map.put(country, map.get(country) + 1);

      // city
      String city = eventProp.location.country_short + "/" + eventProp.location.city;
      map = mailStatsProp.cityVsNumOpens;
      if (!map.containsKey(city))
        map.put(city, 0);

      map.put(city, map.get(city) + 1);

      return;
    }

    if (event.equals("click")) {
      mailStatsProp.numClicks++;

      String url = eventProp.url;
      Map<String, Integer> map = mailStatsProp.urlVsNumClicks;
      if (!map.containsKey(url))
        map.put(url, 0);

      map.put(url, map.get(url) + 1);
      return;
    }

    if (event.equals("spam")) {
      mailStatsProp.numSpams++;
      return;
    }

    // should never get unsub events but just in case
    if (event.equals("unsub")) {
      return;
    }

    if (event.equals("reject")) {
      mailStatsProp.numRejects++;
      return;
    }
  }

  private static void updateSentMailEntity(SentMailEntity sentMailEntity,
      MandrillEventProp eventProp) {

    ensureNotNull(sentMailEntity, "sentMailEntity is null");
    ensureNotNull(eventProp, "mandrillEventProp is null");

    // events are: send, deferral, hard_bounce, soft_bounce, open, click, spam, unsub, reject
    if (eventProp.event == null)
      return;

    String event = eventProp.event;
    if (event.equals("send")) {
      sentMailEntity.status = MailStatus.SENT;
      sentMailEntity.statusTS = eventProp.ts;
      sentMailEntity.sendTS = eventProp.ts;
      return;
    }

    if (event.equals("deferral")) {
      sentMailEntity.status = MailStatus.DEFERRAL;
      sentMailEntity.statusTS = eventProp.ts;
      return;
    }

    if (event.equals("hard_bounce")) {
      sentMailEntity.status = MailStatus.HARD_BOUNCE;
      sentMailEntity.statusTS = eventProp.ts;
      return;
    }

    if (event.equals("soft_bounce")) {
      sentMailEntity.status = MailStatus.SOFT_BOUNCE;
      sentMailEntity.statusTS = eventProp.ts;
      return;
    }

    if (event.equals("open")) {
      sentMailEntity.status = MailStatus.OPENED;
      sentMailEntity.statusTS = eventProp.ts;

      sentMailEntity.opened = true;
      sentMailEntity.country = eventProp.location.country_short;
      sentMailEntity.city = sentMailEntity.country + "/" + eventProp.location.city;
      sentMailEntity.region = eventProp.location.region;
      sentMailEntity.mobile = eventProp.user_agent_parsed.mobile;
      return;
    }

    if (event.equals("click")) {
      sentMailEntity.status = MailStatus.CLICKED;
      sentMailEntity.statusTS = eventProp.ts;

      sentMailEntity.clicked = true;

      if ((eventProp.url != null) && (eventProp.url.length() > 255)) {
        eventProp.url = eventProp.url.substring(0, 255);
      }
      sentMailEntity.clickUrls.add(eventProp.url);
      sentMailEntity.clickTS.add(eventProp.ts);
      return;
    }

    if (event.equals("spam")) {
      sentMailEntity.status = MailStatus.REPORTED_AS_SPAM;
      sentMailEntity.statusTS = eventProp.ts;
      return;
    }

    // should never get unsub events but just in case
    if (event.equals("unsub")) {
      sentMailEntity.status = MailStatus.UNSUB;
      sentMailEntity.statusTS = eventProp.ts;
      return;
    }

    if (event.equals("reject")) {
      sentMailEntity.status = MailStatus.REJECTED_BY_MANDRILL;
      sentMailEntity.statusTS = eventProp.ts;
      return;
    }

    // ignore sync events for now
  }

  private static void ensureValidApiKey(String apiKey) {
    ensureNotNull(apiKey, "apiKey is null");

    MandrillApi mandrillApi = new MandrillApi(apiKey);

    try {
      mandrillApi.users().ping();
    } catch (MandrillApiError | IOException e) {
      throw new APIException().status(Status.ERROR_RESOURCE_NOT_FOUND).message(
          "Mandrill Api Key [" + apiKey + "] is invalid");
    }
  }

  private static void ensureValidSubaccount(String alreadyValidatedApiKey, String subaccount)
      throws IOException {

    ensureNotNull(alreadyValidatedApiKey, "alreadyValidatedApiKey is null");
    ensureNotNull(subaccount, "subaccount is null");

    if (!isSubaccountValid(alreadyValidatedApiKey, subaccount)) {
      throw new APIException().status(Status.ERROR_RESOURCE_NOT_FOUND).message(
          "Subaccount [" + subaccount + "] not found for api key [" + alreadyValidatedApiKey + "]");
    }
  }

  private static boolean isSubaccountValid(String alreadyValidatedApiKey, String subaccount)
      throws IOException {
    ensureNotNull(alreadyValidatedApiKey, "alreadyValidatedApiKey is null");
    ensureNotNull(subaccount, "subaccount is null");

    MandrillApi mandrillApi = new MandrillApi(alreadyValidatedApiKey);

    try {
      mandrillApi.subaccounts().info(subaccount);
    } catch (MandrillApiError e) {
      return false;
    }

    return true;
  }
}
